from datetime import datetime, date, timedelta, time
from typing import List
import pandas as pd
from ortools.linear_solver import pywraplp as lp

from .base_class import WorkingStaff, DispatchTask, DistanceMatrix, \
    Position, PositionType, WorkingVehicle


REST_POS = Position(pos_type=PositionType.Rest, name="r")


class BaseTaskNode:
    def __init__(self,
            task:DispatchTask = None,
            start_time_var = None,
            culmu_worktime_var = None,
            rest_var = None,):
        self.task = task
        self.start_time_var = start_time_var
        self.culmu_worktime_var = culmu_worktime_var
        self.rest_var = rest_var

    def __repr__(self):
        if self.start_time_var:
            time_int = self.start_time_var.solution_value()
            time_str = int_to_timestr(time_int)
            return f"Task_id:{self.task.id} Time:{time_str}"
        else:
            return f"Task_id:{self.task.id}"


class DispatchTaskNode:
    NORMAL = 0
    VIRTUAL = 1

    equip_on_task_id = 25917
    equip_off_task_id = 25918

    def __init__(self, 
            task_index: int = None,
            staff = None,
            staff_index: int = None,
            base_task_node:BaseTaskNode = None,
            type_id: int = 0,
            ):
        self.task_index = task_index
        self.staff_index = staff_index

        self.base_task_node = base_task_node
        self.staff = staff

        self.type_id = type_id

        if self.base_task_node and self.base_task_node.task:
            self.planned_start_time = self.base_task_node.task.planned_start_time
            self.planned_end_time = self.base_task_node.task.planned_end_time
        else:
            self.planned_start_time = None
            self.planned_end_time = None
        
        self.valid_nexts = []
        self.valid_pres = []

    @property
    def is_equip_off_task(self):
        if self.base_task_node.task:
            return self.base_task_node.task.guarantee_item.id == DispatchTaskNode.equip_off_task_id
        else:
            return 0

    def __repr__(self):
        if self.base_task_node.start_time_var:
            time_int = self.base_task_node.start_time_var.solution_value()
            time_str = int_to_timestr(time_int)
            return f"Task:{self.task_index} Time:{time_str}"
        else:
            return f"Staff:{self.staff_index}"


class DispatchFlightNode:
    NORMAL = 0
    VIRTUAL = 1

    def __init__(self, 
            flight_index: int = None,
            equip_index: int = None,
            flight = None,
            equip = None,
            equip_on_task_node:BaseTaskNode = None,
            equip_off_task_node:BaseTaskNode = None,
            type_id: int = 0,
            ):
        self.flight_index = flight_index
        self.equip_index = equip_index

        self.flight = flight
        self.equip = equip

        self.equip_on_task_node = equip_on_task_node
        self.equip_off_task_node = equip_off_task_node
        
        self.type_id = type_id
        
        self.valid_nexts = []
        self.valid_pres = []

    def __repr__(self):
        if self.equip_on_task_node.start_time_var:
            time_int = self.equip_on_task_node.start_time_var.solution_value()
            time_str = int_to_timestr(time_int)
            return f"Flight:{self.flight_index} Time:{time_str}"
        else:
            return f"Equip:{self.equip_index}"


class DispatchSolver:
    def __init__(self,
            dispatch_dt: datetime,
            distance_matrix: DistanceMatrix,
            staff_list: List[WorkingStaff],
            equip_list: List[WorkingVehicle],
            task_list: List,
            standard,
            rules = {
                "worktime_max": 120,
                "rest_time": 10,
            }):
        self._dispatch_dt = dispatch_dt
        self._distance_matrix = distance_matrix
        self._staff_list = staff_list
        self._equip_list = equip_list
        self._task_list = task_list
        self._standard = standard
        self._rules = rules

        # Data preprocessing
        self.sort_task()

        self._flight_list = []
        self.concat_flight()

        # Initialize the problem
        # self._solver = lp.Solver("Dispatch Solver", lp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
        self._solver = lp.Solver("Dispatch Solver", lp.Solver.SCIP_MIXED_INTEGER_PROGRAMMING)
        # self._solver = lp.Solver("Dispatch Solver", lp.Solver.GUROBI_MIXED_INTEGER_PROGRAMMING)

        self.task_cnt = len(task_list)
        self.flight_cnt = len(self._flight_list)
        self.staff_cnt = len(staff_list)
        self.equip_cnt = len(equip_list)

        self._task_node_matrix = []
        self._flight_node_matrix = []
        self._rest_task_node_matrix = []
        self._rest_flight_node_matrix = []
        self._node_cnt = 0

        self._task_arc_matrix = {}
        self._flight_arc_matrix = {}
        self._arc_cnt = 0

        self._M = 1440*2

        self._status = None        

    @property
    def refer_datetime(self):
        return datetime.combine(
            self._task_list[0].planned_start_time.date(),time(0,0))
            
    def sort_task(self):
        self._task_list.sort(key=lambda task: task._planned_start_time)

    def concat_flight(self):
        for task in self._task_list:
            if task.guarantee_item.id == DispatchTaskNode.equip_on_task_id:
                self._flight_list.append(task.flight)
        self.flight_cnt = len(self._flight_list)

    def construct_network(self):
        print("====================")
        print(f"Total number of flights: {self.flight_cnt}")
        print(f"Total number of tasks: {self.task_cnt}")
        print()

        print("====================")
        print("Start generating nodes:")
        # Generate nodes
        # 0. Base task node
        self._base_task_node_list = []
        for i in range(self.task_cnt):
            task = self._task_list[i]
            self._base_task_node_list.append(BaseTaskNode(
                task = task,                    
                start_time_var = self._solver.IntVar(
                    0, self._solver.Infinity(), 
                    f"Task_{i} Arrive"),
                culmu_worktime_var = self._solver.IntVar(
                    0, self._rules["worktime_max"],
                    f"Task_{i} Culmulative Work"),
                rest_var = self._solver.IntVar(
                    0,1,
                    f"Task_{i} -> Rest"),
                )
            )

        # 1. Node per task per staff
        self._task_node_matrix = [[
                DispatchTaskNode(
                    task_index = i,
                    staff_index = j,
                    base_task_node = self._base_task_node_list[i],
                    staff = self._staff_list[j],
                    type_id = DispatchTaskNode.NORMAL,
                )
                for j in range(self.staff_cnt)] 
                for i in range(self.task_cnt)]

        # 2. Special virtual start/end node for each staff
        self._rest_task_node_matrix = [{
                "Virtual_Start": DispatchTaskNode(
                    task_index = None,
                    staff_index = j,
                    base_task_node = BaseTaskNode(
                        task = None,
                        start_time_var = None,
                        culmu_worktime_var=self._solver.IntVar(
                            0, self._rules["worktime_max"],
                            f"Start Staff_{j} Culmulative Work"),
                        rest_var = self._solver.IntVar(
                            0,1,
                            f"Start Staff_{j} => Rest"),
                        ),
                    staff = self._staff_list[j],
                    type_id = DispatchTaskNode.VIRTUAL,
                    ),
                "Virtual_End":  DispatchTaskNode(
                    task_index = None,
                    staff_index = j,
                    base_task_node = BaseTaskNode(
                        task = None,
                        start_time_var = None,
                        culmu_worktime_var=self._solver.IntVar(
                            0, self._rules["worktime_max"],
                            f"End Staff_{j} Culmulative Work"),
                        rest_var = None,
                        ),
                    staff = self._staff_list[j],
                    type_id=DispatchTaskNode.VIRTUAL,
                    )
            } for j in range(self.staff_cnt)]

        # 3. Node per flight per equipment
        for x in range(self.flight_cnt):
            equip_on_task_node = None
            equip_off_task_node = None
            for i in range(self.task_cnt):
                if self._task_list[i].flight != self._flight_list[x]:
                    continue
                guarantee_id = self._task_list[i].guarantee_item.id
                # retrieve both equip_on and equip_off task nodes
                if guarantee_id == DispatchTaskNode.equip_on_task_id:
                    equip_on_task_node = self._base_task_node_list[i]
                elif guarantee_id == DispatchTaskNode.equip_off_task_id:
                    equip_off_task_node = self._base_task_node_list[i]
                if equip_on_task_node and equip_off_task_node:
                    break

            self._flight_node_matrix.append([
                DispatchFlightNode( 
                    flight_index = x,
                    equip_index = y,
                    flight = self._flight_list[x],
                    equip = self._equip_list[y],
                    equip_on_task_node = equip_on_task_node,
                    equip_off_task_node = equip_off_task_node,
                    type_id = DispatchFlightNode.NORMAL,
                )
                for y in range(self.equip_cnt)])

        # 4. Special virtual start/end node for each equipment
        self._rest_flight_node_matrix = [{
                "Virtual_Start": DispatchFlightNode(
                    flight_index = None,
                    equip_index = y,
                    flight = None,
                    equip = self._equip_list[y],
                    equip_on_task_node = None,
                    equip_off_task_node = None,
                    type_id=DispatchFlightNode.VIRTUAL,
                    ),
                "Virtual_End":  DispatchFlightNode(
                    flight_index = None,
                    equip_index = y,
                    flight = None,
                    equip = self._equip_list[y],
                    equip_on_task_node = None,
                    equip_off_task_node = None,
                    type_id=DispatchFlightNode.VIRTUAL,
                    )
            } for y in range(self.equip_cnt)]
                  
        print(f"    Base Task Nodes count: {self.task_cnt}")                               
        print(f"    Task Nodes count: {self.task_cnt * self.staff_cnt + self.staff_cnt * 2}")                 
        print(f"    Flight Nodes count: {self.flight_cnt * self.equip_cnt + self.equip_cnt * 2}")                       
        print(f"    Base Task Nodes variable count: {self.task_cnt * 3}")             
        print(f"    Task Nodes variable count: {self.staff_cnt * 3}")
        print(f"    Flight Nodes variable count: {0}")
        print()
        print("====================")
        print("Start generating arcs:")

        # Generate arcs between tasks and arcs between flights
        arc_prefix = "    Arcs created: "

        # 1. Arcs between normal task nodes
        cnt = 0
        for j in range(self.staff_cnt):
            for i_1 in range(self.task_cnt - 1):
                for i_2 in range(i_1+1, self.task_cnt):
                    node_1 = self._task_node_matrix[i_1][j]
                    node_2 = self._task_node_matrix[i_2][j]
                    self._task_arc_matrix[(node_1, node_2)] = {
                        "arc_var": self._solver.IntVar(0, 1, f"Task_{i_1} Staff_{j} -> Task_{i_2} Staff_{j}"),
                    }
                    
                    node_1.valid_nexts.append(node_2)
                    node_2.valid_pres.append(node_1)

                    cnt = self.add_check_cnt(cnt, arc_prefix)
        
        # 2. Arcs between virtual start/end task nodes and normal task nodes
        for j in range(self.staff_cnt):
            v_start_node = self._rest_task_node_matrix[j]["Virtual_Start"]
            v_end_node = self._rest_task_node_matrix[j]["Virtual_End"]

            for i in range(self.task_cnt):
                node = self._task_node_matrix[i][j]

                self._task_arc_matrix[(v_start_node, node)] = {
                    "arc_var": self._solver.IntVar(0, 1, f"Start -> Task_{i} Staff_{j}"),
                }
                self._task_arc_matrix[(node, v_end_node)] = {
                    "arc_var": self._solver.IntVar(0, 1, f"Task_{i} Staff_{j} -> End"),
                }

                v_start_node.valid_nexts.append(node)
                node.valid_pres.append(v_start_node)

                node.valid_nexts.append(v_end_node)
                v_end_node.valid_pres.append(node)

                cnt = self.add_check_cnt(cnt, arc_prefix)
                cnt = self.add_check_cnt(cnt, arc_prefix)

        # 3. Arcs between normal flight nodes
        for y in range(self.equip_cnt):
            for x_1 in range(self.flight_cnt - 1):
                for x_2 in range(x_1+1, self.flight_cnt):
                    node_1 = self._flight_node_matrix[x_1][y]
                    node_2 = self._flight_node_matrix[x_2][y]
                    self._flight_arc_matrix[(node_1, node_2)] = {
                        "arc_var": self._solver.IntVar(0, 1, f"flight_{x_1} Equipment_{y} -> flight_{x_2} Equipment_{y}"),
                    }
                        
                    node_1.valid_nexts.append(node_2)
                    node_2.valid_pres.append(node_1)

                    cnt = self.add_check_cnt(cnt, arc_prefix)
        
        # 4. Arcs between virtual start/end flight nodes and normal flight nodes
        for y in range(self.equip_cnt):
            v_start_node = self._rest_flight_node_matrix[y]["Virtual_Start"]
            v_end_node = self._rest_flight_node_matrix[y]["Virtual_End"]

            for x in range(self.flight_cnt):
                node = self._flight_node_matrix[x][y]

                self._flight_arc_matrix[(v_start_node, node)] = {
                    "arc_var": self._solver.IntVar(0, 1, f"Start -> Flight_{x} Euqipment_{y}"),
                }
                self._flight_arc_matrix[(node, v_end_node)] = {
                    "arc_var": self._solver.IntVar(0, 1, f"Flight_{x} Euqipment_{y} -> End"),
                }

                v_start_node.valid_nexts.append(node)
                node.valid_pres.append(v_start_node)

                node.valid_nexts.append(v_end_node)
                v_end_node.valid_pres.append(node)

                cnt = self.add_check_cnt(cnt, arc_prefix)
                cnt = self.add_check_cnt(cnt, arc_prefix)

        print(f"    Arcs created: {cnt}")
        print()

        # print("===Debug Info===")
        # for i in range(self.task_cnt):
        #     for j in range(self.staff_cnt):
        #         node_1 = self._node_matrix[i][j]
        #         node_1_out_arc_cnt = len(node_1.valid_nexts)
        #         node_1_in_arc_cnt = len(node_1.valid_pres)
        #         print(f"{node_1}, out-arc {node_1_out_arc_cnt}, in-arc {node_1_in_arc_cnt}")


    def add_constraints(self):
        print("====================")
        print("Start generating constraints:")
        cnt = 0
        constraint_prefix = "    Constraints created: "

        # 1. Each staff and equipment start work from v_start_node and end work at v_end_node
        # 1.1 for staff
        for j in range(self.staff_cnt):
            v_start_node = self._rest_task_node_matrix[j]["Virtual_Start"]
            v_end_node = self._rest_task_node_matrix[j]["Virtual_End"]

            # For each staff, choose one and only one task as his first task
            self._solver.Add(self._solver.Sum(
                [self._task_arc_matrix[(v_start_node, node)]["arc_var"] for node in v_start_node.valid_nexts]) == 1)

            cnt = self.add_check_cnt(cnt, constraint_prefix)

            # For each staff, choose one and only one task as his last task
            self._solver.Add(self._solver.Sum(
                [self._task_arc_matrix[(node, v_end_node)]["arc_var"] for node in v_end_node.valid_pres]) == 1)

            cnt = self.add_check_cnt(cnt, constraint_prefix)
        
        # 1.2 for equipment
        for y in range(self.equip_cnt):
            v_start_node = self._rest_flight_node_matrix[y]["Virtual_Start"]
            v_end_node = self._rest_flight_node_matrix[y]["Virtual_End"]

            # For each equipment, choose one and only one task as its first task
            self._solver.Add(self._solver.Sum(
                [self._flight_arc_matrix[(v_start_node, node)]["arc_var"] for node in v_start_node.valid_nexts]) == 1)

            cnt = self.add_check_cnt(cnt, constraint_prefix)

            # For each equipment, choose one and only one task as its last task
            self._solver.Add(self._solver.Sum(
                [self._flight_arc_matrix[(node, v_end_node)]["arc_var"] for node in v_end_node.valid_pres]) == 1)

            cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 2. Network flow conservation
        # 2.1 for staff
        for i in range(self.task_cnt):
            for j in range(self.staff_cnt):
                node_1 = self._task_node_matrix[i][j]
                self._solver.Add(
                    self._solver.Sum([self._task_arc_matrix[(node_1, node_2)]["arc_var"] for node_2 in node_1.valid_nexts]) ==
                    self._solver.Sum([self._task_arc_matrix[(node_2, node_1)]["arc_var"] for node_2 in node_1.valid_pres]))

                cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 2.2 for equipment
        for x in range(self.flight_cnt):
            for y in range(self.equip_cnt):
                node_1 = self._flight_node_matrix[x][y]
                self._solver.Add(
                    self._solver.Sum([self._flight_arc_matrix[(node_1, node_2)]["arc_var"] for node_2 in node_1.valid_nexts]) ==
                    self._solver.Sum([self._flight_arc_matrix[(node_2, node_1)]["arc_var"] for node_2 in node_1.valid_pres]))

                cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 3. Task start_time constraints for normal tasks
        for i in range(self.task_cnt):
            node = self._base_task_node_list[i]
            start_int = minutes_from_calibr(node.task.planned_start_time, self.refer_datetime)
            self._solver.Add(node.start_time_var >= start_int)
            cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 4. Runtime, service duration, and culmulative_worktime constraints between tasks
        # 4.1 for staff
        for j in range(self.staff_cnt):
            for i_1 in range(self.task_cnt):
                node_1 = self._task_node_matrix[i_1][j]
                node_1_task = node_1.base_task_node.task
                for node_2 in node_1.valid_nexts:
                    if node_2.type_id != DispatchTaskNode.VIRTUAL:
                        node_2_task = node_2.base_task_node.task
                        run_time_between = int(self._distance_matrix.get_distance(
                            node_1_task.end_pos, node_2_task.start_pos))

                        node_1_operation_int = int(node_1_task.duration.total_seconds()/60)
                        node_2_operation_int = int(node_2_task.duration.total_seconds()/60)
                        arc_var = self._task_arc_matrix[(node_1, node_2)]["arc_var"]
                        rest_var = node_1.base_task_node.rest_var

                        # runtime constraint
                        cons = self._solver.Constraint(run_time_between*node_2.is_equip_off_task+node_1_operation_int-self._M, self._solver.Infinity())
                        cons.SetCoefficient(node_2.base_task_node.start_time_var, 1)
                        cons.SetCoefficient(node_1.base_task_node.start_time_var, -1)
                        cons.SetCoefficient(arc_var, -self._M)                        
                        cons.SetCoefficient(rest_var, -self._rules["rest_time"])  
                        for y in range(self.equip_cnt):
                            start_flight_node = self._rest_flight_node_matrix[y]["Virtual_Start"]
                            x = self._flight_list.index(node_2_task.flight)
                            flight_node = self._flight_node_matrix[x][y]
                            flight_arc_var = self._flight_arc_matrix[(start_flight_node,flight_node)]["arc_var"]
                            run_time_between = int(self._distance_matrix.get_distance(
                                node_1_task.end_pos, REST_POS))
                            run_time_between += int(self._distance_matrix.get_distance(
                                REST_POS, node_2_task.start_pos))
                            cons.SetCoefficient(flight_arc_var, -run_time_between * (1-node_2.is_equip_off_task))

                        for y in range(self.equip_cnt):
                            x_2 = self._flight_list.index(node_2_task.flight)
                            flight_node_2 = self._flight_node_matrix[x_2][y]
                            for flight_node_1 in flight_node_2.valid_pres:
                                if not flight_node_1.flight:
                                    continue
                                flight_arc_var = self._flight_arc_matrix[(flight_node_1,flight_node_2)]["arc_var"]
                                run_time_between = int(self._distance_matrix.get_distance(
                                    node_1_task.end_pos, flight_node_1.equip_off_task_node.task.end_pos))
                                run_time_between += int(self._distance_matrix.get_distance(
                                    flight_node_1.equip_off_task_node.task.end_pos, node_2_task.start_pos))
                                cons.SetCoefficient(flight_arc_var, -run_time_between * (1-node_2.is_equip_off_task))

                        cnt = self.add_check_cnt(cnt, constraint_prefix)

                        # culmulative worktime constraint
                        self._solver.Add(
                            node_2.base_task_node.culmu_worktime_var >= node_1.base_task_node.culmu_worktime_var +
                                                        node_2.base_task_node.start_time_var - node_1.base_task_node.start_time_var -
                                                        self._M * rest_var -
                                                        self._M * (1 - arc_var)
                        )

                        cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 4.2 for equipment
        for y in range(self.equip_cnt):
            for x_1 in range(self.flight_cnt):
                node_1 = self._flight_node_matrix[x_1][y]
                for node_2 in node_1.valid_nexts:
                    if node_2.type_id != DispatchFlightNode.VIRTUAL:
                        run_time_between = int(self._distance_matrix.get_distance(
                            node_1.equip_off_task_node.task.end_pos, node_2.equip_on_task_node.task.start_pos))

                        node_1_on_operation_int = int(node_1.equip_on_task_node.task.duration.total_seconds()/60)
                        node_1_off_operation_int = int(node_1.equip_off_task_node.task.duration.total_seconds()/60)
                        arc_var = self._flight_arc_matrix[(node_1, node_2)]["arc_var"]

                        # runtime constraint
                        self._solver.Add(
                            node_2.equip_on_task_node.start_time_var - node_1.equip_off_task_node.start_time_var >= 
                            arc_var * (run_time_between + node_1_off_operation_int) - (1 - arc_var) * self._M
                        )

                # service duration constraint
                self._solver.Add(
                    node_1.equip_off_task_node.start_time_var - node_1.equip_on_task_node.start_time_var >= 
                    node_1_on_operation_int + 30 # 30: minimum service time
                )

                cnt = self.add_check_cnt(cnt, constraint_prefix)
        
                        
        # 5. Runtime and culmulative_worktime constraints for start/end tasks
        for j in range(self.staff_cnt):
            v_start_node = self._rest_task_node_matrix[j]["Virtual_Start"]
            v_end_node = self._rest_task_node_matrix[j]["Virtual_End"]

            work_start_int = minutes_from_calibr(
                    self._staff_list[j]._work_start_time, self.refer_datetime)
            work_end_int = minutes_from_calibr(
                    self._staff_list[j]._work_end_time, self.refer_datetime)

            # !! Temporarily use "REST_POS" now 
            for node_2 in v_start_node.valid_nexts:
                run_time_between = int(self._distance_matrix.get_distance(
                    REST_POS, node_2.base_task_node.task.start_pos))

                arc_var = self._task_arc_matrix[(v_start_node, node_2)]["arc_var"]
                rest_var = v_start_node.base_task_node.rest_var
                node_2_operation_int = int(node_2.base_task_node.task.duration.total_seconds()/60)

                # runtime constraint
                self._solver.Add(
                    node_2.base_task_node.start_time_var >= (run_time_between + work_start_int) * arc_var + 
                                              rest_var * self._rules["rest_time"]
                )

                cnt = self.add_check_cnt(cnt, constraint_prefix)

                # culmulative worktime constraint
                self._solver.Add(
                    node_2.base_task_node.culmu_worktime_var >= v_start_node.base_task_node.culmu_worktime_var +
                                                 run_time_between + node_2_operation_int -
                                                 self._M * rest_var -
                                                 self._M * (1 - arc_var)
                )

                cnt = self.add_check_cnt(cnt, constraint_prefix)

            for node_3 in v_end_node.valid_pres:
                run_time_between = int(self._distance_matrix.get_distance(
                    REST_POS, node_3.base_task_node.task.end_pos))

                arc_var = self._task_arc_matrix[(node_3, v_end_node)]["arc_var"]
                rest_var = node_3.base_task_node.rest_var
                node_3_operation_int = int(node_3.base_task_node.task.duration.total_seconds()/60)
                latest_time = (work_end_int - run_time_between - node_3_operation_int)

                # start time constraint
                self._solver.Add(node_3.base_task_node.start_time_var <= 
                    latest_time * arc_var + (1 - arc_var) * self._M + self._rules["rest_time"] * rest_var)

                cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 6. Task covering constraints
        # 6.1 for staff: each task visited by one staff
        for i in range(self.task_cnt):
            arcs = []
            for j in range(self.staff_cnt):
                node_1 = self._task_node_matrix[i][j]
                for node_2 in node_1.valid_nexts:
                    arcs.append({
                        "var": self._task_arc_matrix[(node_1, node_2)]["arc_var"],
                        "staff_index": j,
                    })
            
            cons = self._solver.Constraint(1,1)

            for arc in arcs:
                cons.SetCoefficient(arc["var"], 1)

            cnt = self.add_check_cnt(cnt, constraint_prefix)

        # 6.2 for equipment: each flight served by one equipment
        for x in range(self.flight_cnt):
            arcs = []
            for y in range(self.equip_cnt):
                node_1 = self._flight_node_matrix[x][y]
                for node_2 in node_1.valid_nexts:
                    arcs.append({
                        "var": self._flight_arc_matrix[(node_1, node_2)]["arc_var"],
                        "staff_index": j,
                    })
            cons = self._solver.Constraint(1,1)

            for arc in arcs:
                cons.SetCoefficient(arc["var"], 1)

        print(f"{constraint_prefix}{cnt}")
        print()                        

    def set_objective(self):
        obj = self._solver.Objective()

        offset = 0
        for i in range(self.task_cnt):
            for j in range(self.staff_cnt):
                node = self._task_node_matrix[i][j].base_task_node
                obj.SetCoefficient(node.start_time_var, 1)
                offset -= minutes_from_calibr(node.task.planned_start_time, self.refer_datetime)

        obj.SetOffset(offset) 
        obj.SetMinimization()

    def solve(self, num_theads):
        self._solver.EnableOutput()
        # self._solver.SetNumThreads(num_theads)
        self._status = self._solver.Solve()
        self.print_result()

    def print_result(self):
        print("===Result Info===")
        if self._status  == lp.Solver.OPTIMAL or self._status == lp.Solver.FEASIBLE:
            if self._status == lp.Solver.FEASIBLE:
                print("Find Feasible")
            if self._status == lp.Solver.OPTIMAL:
                print("Find Optimal")

            print('Solution:')
            print('Objective value =', self._solver.Objective().Value())
            print()

            path_list = []
            for rest in self._rest_task_node_matrix:
                node = rest["Virtual_Start"]
                path = [str(node)]

                cur_node = node
                while True:
                    nexts = cur_node.valid_nexts
                    for next_node in nexts:
                        arc_var = self._task_arc_matrix[(cur_node, next_node)]["arc_var"]
                        rest_var = cur_node.base_task_node.rest_var
                        if arc_var.solution_value() == 1:
                            if rest_var.solution_value() == 1:
                                path.append("rest")
                            culmu_workint_time = next_node.base_task_node.culmu_worktime_var.solution_value()
                            path.append(str(next_node) 
                                + ' ' + str(int(culmu_workint_time)) 
                                + ' ' + str(int(arc_var.solution_value())) 
                                + ' ' + str(int(rest_var.solution_value())))
                            cur_node = next_node
                            break

                    if cur_node.type_id == DispatchTaskNode.VIRTUAL:
                        break 

                path_list.append(path)

            for path in path_list:
                print('---')
                print(" -> ".join(path))

    def add_check_cnt(self, cnt, prefix):
        cnt += 1
        if not (cnt % 10000):
            print(f"{prefix}{cnt}")
        return cnt


def minutes_from_calibr(input_datetime, refer_datetime):
    return int((input_datetime - refer_datetime).total_seconds()/60)

def int_to_timestr(input_int):
    hour = str(int(input_int // 60)).rjust(2, "0")
    minute = str(int(input_int % 60)).rjust(2, "0")
    return f"{hour}:{minute}"